package rwt.server.core;

import java.io.*;
import java.net.*;
import java.util.Arrays;

import org.slf4j.*;

import rwt.server.RailServer;

/**
 * Represents a TCP/IP socket connection. Uses blocking network I/O on a separate thread to listen for
 * incoming messages from a TCP server (such as the railroad controller program).
 */
public class TcpConnection
{
	private static final Logger logger = LoggerFactory.getLogger(TcpConnection.class);
	
	public static final String LOCALHOST = "127.0.0.1";
	private static final int BUFFER_SIZE = 1024;
	// private static final int MAX_PACKET_SIZE = 65536; // 64 KB
	
	private Socket socket;
	private String address;
	private int port;
	private boolean debugOn = true;
	private boolean isClosing = false;
	
	private ITcpSocketListener listener;
	private byte[] buffer = new byte[BUFFER_SIZE];
	
	private Thread inputStreamReaderThread;
	private BufferedInputStream in;
	private BufferedOutputStream out;
	
	/**
	 * Creates a new, unconnected TCP connection.
	 * 
	 * @param address The IP address of the TCP destination.
	 * @param port Port number of the TCP destination.
	 * @param listener The listener to receive callbacks when a message arrives from the destination.
	 */
	public TcpConnection(String address, int port, ITcpSocketListener listener)
	{
		this.address = address;
		this.port = port;
		this.listener = listener;
	}
	
	public void reset()
	{
		this.isClosing = false;
	}
	
	/**
	 * Attempts to connect to the TCP destination.
	 * 
	 * @return true if the connection succeeds; false if the connection could not be established.
	 */
	public boolean connect()
	{
		try
		{
			Socket testSocket = new Socket();
			testSocket.connect(new InetSocketAddress(address, port), 50);
			attachSocket(testSocket);
			startListening();
			return true;
		}
		catch (IOException e)
		{
			logger.warn("Failed to connect to " + address + ":" + port, e);
			return false;
		}
	}
	
	/**
	 * 
	 * 
	 * @param socket
	 * @throws IOException
	 */
	private void attachSocket(Socket socket) throws IOException
	{
		this.socket = socket;
		socket.setSendBufferSize(512);
		socket.setReceiveBufferSize(512);
		in = new BufferedInputStream(socket.getInputStream(), BUFFER_SIZE);
		out = new BufferedOutputStream(socket.getOutputStream(), BUFFER_SIZE);
	}
	
	/**
	 * Determines if this TCP connection is alive.
	 * 
	 * @return true if the connection is alive; false otherwise.
	 */
	public boolean isConnected()
	{
		return (socket != null && socket.isConnected() && !socket.isClosed());
	}
	
	private void startListening()
	{
		inputStreamReaderThread = new Thread(new InputStreamReaderTask());
		inputStreamReaderThread.start();
	}
	
	/**
	 * A task that receives messages from the TCP destination. Messages are passed to the listener.
	 */
	private class InputStreamReaderTask implements Runnable
	{
		@Override
		public void run()
		{
			print("Listening for incoming railroad messages...");
			
			while (!isClosing)
			{
				try
				{
					int read = in.read(buffer);
					if (read == -1)
					{
						// End of stream
						print("End of stream");
						close();
						return;
					}
					
					if (read > BUFFER_SIZE)
					{
						print("Incoming message exceeded buffer size of " + BUFFER_SIZE);
					}
					else if (read > 0)
					{
						byte[] data = Arrays.copyOf(buffer, read);
						listener.onTcpMessage(data);
					}
					else
					{
						print("EMPTY LINE");
					}
				}
				catch (SocketException e)
				{
					logger.info("Socket was closed.");
					close();
					break;
				}
				catch (IOException e)
				{
					logger.warn("", e);
					break;
				}
			}
			
			print("Exiting...");
		}
	}
	
	/**
	 * Sends a data packet (array of bytes) to the TCP destination. A two-byte length marker is added to the
	 * front of the packet to match the behavior of "39dll" used by the Controller program.
	 * 
	 * @param data The data packet
	 */
	public void send(byte[] data)
	{
		try
		{
			out.write(data.length & 0xff);
			out.write((data.length >> 8) & 0xff);
			out.write(data, 0, data.length);
			out.flush();
		}
		catch (IOException e)
		{
			logger.warn("", e);
		}
	}
	
	/**
	 * Closes the TCP socket, if open, with any open input and output streams.
	 */
	public void close()
	{
		print("Closing...");
		isClosing = true;
		
		try
		{
			if (socket != null)
			{
				if (in != null)
					in.close();
				if (out != null)
					out.close();
				socket.close();
				socket = null;
				
				inputStreamReaderThread.join();
				print("TcpConnection: In stream thread joined!");
			}
		}
		catch (IOException | InterruptedException e)
		{
			logger.warn("", e);
		}
	}
	
	private void print(String msg)
	{
		if (debugOn)
		{
			logger.info("TcpConnection: " + msg);
		}
	}
}
